<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>VDO.Ninja Developer API Sandbox</title>
  <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg-primary: #1a1a1a;
      --bg-secondary: #252525;
      --bg-tertiary: #2d2d2d;
      --text-primary: #e0e0e0;
      --text-secondary: #a0a0a0;
      --accent-primary: #6366f1;
      --accent-secondary: #818cf8;
      --accent-success: #10b981;
      --accent-warning: #f59e0b;
      --accent-danger: #ef4444;
      --border-color: #404040;
      --radius: 4px;
      --spacing: 8px;
    }
    
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
      color: var(--text-primary);
      background-color: var(--bg-primary);
      line-height: 1.5;
      padding: var(--spacing);
    }
    
    .container {
      display: grid;
      grid-template-columns: 3fr 1fr;
      grid-template-rows: auto 1fr;
      grid-template-areas: 
        "header header"
        "main logger";
      gap: var(--spacing);
      height: calc(100vh - var(--spacing) * 2);
      max-width: 100%;
    }
    
    header {
      grid-area: header;
      padding: calc(var(--spacing) * 2);
      background-color: var(--bg-secondary);
      border-radius: var(--radius);
      border: 1px solid var(--border-color);
    }
    
    main {
      grid-area: main;
      overflow-y: auto;
      padding: var(--spacing);
      background-color: var(--bg-secondary);
      border-radius: var(--radius);
      border: 1px solid var(--border-color);
    }
    
    .logger-container {
      grid-area: logger;
      display: flex;
      flex-direction: column;
      background-color: var(--bg-secondary);
      border-radius: var(--radius);
      border: 1px solid var(--border-color);
    }
    
    h1, h2, h3, h4 {
      font-weight: 600;
      margin-bottom: calc(var(--spacing) * 1.5);
      color: var(--text-primary);
    }
    
    h1 {
      font-size: 1.8rem;
      color: var(--accent-primary);
    }
    
    h2 {
      font-size: 1.25rem;
      margin-top: calc(var(--spacing) * 3);
      border-bottom: 1px solid var(--border-color);
      padding-bottom: var(--spacing);
    }
    
    h3 {
      font-size: 1rem;
      margin-top: calc(var(--spacing) * 2);
    }
    
    a {
      color: var(--accent-secondary);
      text-decoration: none;
    }
    
    a:hover {
      text-decoration: underline;
    }
    
    p {
      margin-bottom: var(--spacing);
    }
    
    small {
      font-size: 0.85rem;
      color: var(--text-secondary);
    }
    
    pre, code {
      font-family: 'Fira Code', monospace;
    }
    
    pre {
      background-color: var(--bg-tertiary);
      padding: var(--spacing);
      border-radius: var(--radius);
      overflow-x: auto;
      white-space: pre-wrap;
      margin-bottom: var(--spacing);
    }
    
    .api-key {
      font-family: 'Fira Code', monospace;
      padding: calc(var(--spacing) * 0.75) var(--spacing);
      background-color: var(--bg-tertiary);
      border-radius: var(--radius);
      display: inline-block;
      margin: calc(var(--spacing) * 0.5) 0;
      cursor: pointer;
      user-select: all;
      border: 1px solid var(--border-color);
    }
    
    /* Tabs styling */
    .tabs {
      display: flex;
      margin-bottom: var(--spacing);
      border-bottom: 1px solid var(--border-color);
      flex-wrap: wrap;
    }
    
    .tab {
      padding: calc(var(--spacing) * 0.75) calc(var(--spacing) * 1.5);
      font-weight: 500;
      cursor: pointer;
      position: relative;
      border-bottom: 2px solid transparent;
      font-size: 0.9rem;
    }
    
    .tab.active {
      border-bottom: 2px solid var(--accent-primary);
      color: var(--accent-primary);
    }
    
    .tab-content {
      display: none;
    }
    
    .tab-content.active {
      display: block;
    }
    
    /* Button grid */
    .button-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
      gap: var(--spacing);
      margin-bottom: calc(var(--spacing) * 2);
    }
    
    /* Button styling */
    button {
      background-color: var(--bg-tertiary);
      color: var(--text-primary);
      border: 1px solid var(--border-color);
      border-radius: var(--radius);
      padding: calc(var(--spacing) * 0.75) var(--spacing);
      font-size: 0.875rem;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s ease;
      min-height: 3.5rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    }
    
    button:hover {
      background-color: #333;
      border-color: var(--accent-secondary);
    }
    
    button:active {
      transform: translateY(1px);
    }
    
    button[data-type="true"] {
      border-left: 3px solid var(--accent-success);
    }
    
    button[data-type="false"] {
      border-left: 3px solid var(--accent-danger);
    }
    
    button[data-type="toggle"] {
      border-left: 3px solid var(--accent-warning);
    }
    
    .section {
      padding: calc(var(--spacing) * 2);
      margin-bottom: calc(var(--spacing) * 2);
      border-radius: var(--radius);
      background-color: var(--bg-tertiary);
      border: 1px solid var(--border-color);
    }
    
    .section-title {
      display: flex;
      align-items: center;
      margin-bottom: calc(var(--spacing) * 1.5);
      font-weight: 600;
      color: var(--accent-primary);
      padding-bottom: var(--spacing);
      border-bottom: 1px solid var(--border-color);
    }
    
    /* PTZ Controls */
    .ptz-controls {
      display: flex;
      flex-wrap: wrap;
      gap: calc(var(--spacing) * 2);
      margin-top: var(--spacing);
    }
    
    .control-group {
      flex: 1;
      min-width: 200px;
    }
    
    .slider-wrapper {
      margin-bottom: var(--spacing);
    }
    
    .slider-label {
      display: block;
      margin-bottom: calc(var(--spacing) * 0.5);
      font-weight: 500;
      font-size: 0.9rem;
    }
    
    .slider-container {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 0.5);
    }
    
    .slider {
      flex: 1;
      -webkit-appearance: none;
      width: 100%;
      height: 6px;
      border-radius: 3px;
      background: var(--border-color);
      outline: none;
    }
    
    .slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: var(--accent-primary);
      cursor: pointer;
    }
    
    .slider-value {
      font-size: 0.875rem;
      min-width: 40px;
      text-align: center;
      font-family: 'Fira Code', monospace;
    }
    
    /* Request/Response Logger */
    .api-logger-header {
      padding: var(--spacing);
      border-bottom: 1px solid var(--border-color);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .api-logger {
      flex: 1;
      overflow-y: auto;
      overflow-x: hidden;
      background-color: #111;
      color: #ddd;
      font-family: 'Fira Code', monospace;
      padding: var(--spacing);
      font-size: 0.8rem;
    }
    
    .log-entry {
      margin-bottom: calc(var(--spacing) * 0.75);
      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
      padding-bottom: calc(var(--spacing) * 0.5);
      word-wrap: break-word;
    }
    
    .log-time {
      color: #a8a8a8;
    }
    
    .log-method {
      display: inline-block;
      padding: 2px 4px;
      border-radius: 3px;
      margin: 0 4px;
      font-weight: 600;
      font-size: 0.75rem;
    }
    
    .log-get {
      background: #388e3c;
      color: white;
    }
    
    .log-post {
      background: #1976d2;
      color: white;
    }
    
    .log-ws {
      background: #7b1fa2;
      color: white;
    }
    
    .log-sse {
      background: #c2185b;
      color: white;
    }
    
    .log-midi {
      background: #ff8f00;
      color: white;
    }
    
    .log-request {
      color: #64b5f6;
    }
    
    .log-response {
      color: #81c784;
    }
    
    .log-error {
      color: #ef5350;
    }
    
    /* Tooltip */
    .tooltip {
      position: relative;
      cursor: pointer;
    }
    
    .tooltip .tooltip-text {
      visibility: hidden;
      width: 320px;
      background-color: #333;
      color: white;
      text-align: left;
      border-radius: var(--radius);
      padding: calc(var(--spacing) * 0.75);
      position: absolute;
      z-index: 100;
      bottom: 125%;
      left: 50%;
      transform: translateX(-50%);
      opacity: 0;
      transition: opacity 0.2s;
      font-size: 0.75rem;
      font-family: 'Fira Code', monospace;
      white-space: pre-wrap;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    }
    
    .tooltip:hover .tooltip-text {
      visibility: visible;
      opacity: 1;
    }
    
    /* Connection status */
    .connection-status {
      display: inline-flex;
      align-items: center;
      padding: 4px 8px;
      border-radius: 12px;
      font-size: 0.85rem;
      margin-left: var(--spacing);
      background-color: var(--bg-tertiary);
      border: 1px solid var(--border-color);
    }
    
    .status-indicator {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      margin-right: calc(var(--spacing) * 0.75);
    }
    
    .status-connected {
      background-color: var(--accent-success);
    }
    
    .status-disconnected {
      background-color: var(--accent-danger);
    }
    
    .status-connecting {
      background-color: var(--accent-warning);
    }
    
    /* API Request Builders */
    .request-builder {
      background-color: var(--bg-tertiary);
      padding: var(--spacing);
      border-radius: var(--radius);
      margin-bottom: calc(var(--spacing) * 2);
      border: 1px solid var(--border-color);
    }
    
    .form-group {
      margin-bottom: var(--spacing);
    }
    
    label {
      display: block;
      margin-bottom: 4px;
      font-size: 0.9rem;
    }
    
    select, input[type="text"], textarea {
      width: 100%;
      padding: calc(var(--spacing) * 0.75);
      background-color: var(--bg-primary);
      border: 1px solid var(--border-color);
      border-radius: var(--radius);
      color: var(--text-primary);
      font-family: 'Fira Code', monospace;
      font-size: 0.9rem;
    }
    
    textarea {
      min-height: 100px;
      resize: vertical;
    }
    
    .flex-row {
      display: flex;
      gap: var(--spacing);
    }
    
    .flex-row > * {
      flex: 1;
    }

    /* Checkbox styling */
    .checkbox-wrapper {
      display: flex;
      align-items: center;
      margin-bottom: calc(var(--spacing) * 0.5);
    }
    
    .checkbox-wrapper input[type="checkbox"] {
      margin-right: 6px;
    }
    
    /* MIDI config */
    .midi-config {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: var(--spacing);
      margin-top: var(--spacing);
    }
    
    .midi-mapping {
      background-color: var(--bg-tertiary);
      padding: var(--spacing);
      border-radius: var(--radius);
      border: 1px solid var(--border-color);
    }
    
    .midi-mapping-title {
      font-weight: 600;
      margin-bottom: calc(var(--spacing) * 0.5);
    }
    
    .midi-mapping-details {
      font-size: 0.85rem;
      color: var(--text-secondary);
    }
    
    /* Code examples */
    .code-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: calc(var(--spacing) * 0.5) var(--spacing);
      background-color: #333;
      border-top-left-radius: var(--radius);
      border-top-right-radius: var(--radius);
      font-size: 0.8rem;
      border: 1px solid var(--border-color);
      border-bottom: none;
    }
    
    .code-block {
      background-color: #111;
      padding: var(--spacing);
      border-radius: 0 0 var(--radius) var(--radius);
      overflow-x: auto;
      margin-bottom: var(--spacing);
      border: 1px solid var(--border-color);
      font-size: 0.85rem;
    }
    
    .copy-btn {
      background-color: var(--bg-tertiary);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
      padding: 2px 6px;
      font-size: 0.7rem;
      border-radius: 3px;
      cursor: pointer;
    }
    
    .copy-btn:hover {
      background-color: #444;
    }
    
    table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: calc(var(--spacing) * 2);
      font-size: 0.9rem;
    }
    
    th, td {
      padding: calc(var(--spacing) * 0.75);
      text-align: left;
      border-bottom: 1px solid var(--border-color);
    }
    
    th {
      background-color: var(--bg-tertiary);
      font-weight: 600;
    }
    
    tr:nth-child(even) {
      background-color: rgba(255, 255, 255, 0.03);
    }
    
    tr:hover {
      background-color: rgba(255, 255, 255, 0.05);
    }
    
    /* Responsive adjustments */
    @media (max-width: 1024px) {
      .container {
        grid-template-columns: 1fr;
        grid-template-rows: auto 1fr auto;
        grid-template-areas: 
          "header"
          "main"
          "logger";
      }
      
      .logger-container {
        height: 300px;
      }
    }
    
    @media (max-width: 768px) {
      .button-grid {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
      }
      
      .ptz-controls {
        flex-direction: column;
        gap: var(--spacing);
      }
    }
  </style>
</head>
<body>
<div class="container">
  <header>
    <h1>VDO.Ninja Developer API Sandbox</h1>
    <div style="display: flex; flex-wrap: wrap; gap: var(--spacing); align-items: center;">
      <div>
        <p>Your API Key: <span class="api-key" id="api-key" onclick="copyToClipboard(this)">Loading...</span></p>
        <div id="connection-container">
          <span class="connection-status">
            <span class="status-indicator status-connecting" id="connection-indicator"></span>
            <span id="connection-status">Connecting...</span>
          </span>
        </div>
      </div>
      <div style="flex: 1;">
        <p>Your Ninja Link: <a id="ninja-link" href="#" target="_blank">Loading...</a></p>
        <p><small>You can append VDO.Ninja parameters to this link, treating it like a normal VDO.Ninja link.</small></p>
      </div>
      <div>
        <a href="https://github.com/bitfocus/companion-module-vdo-ninja" target="_blank" style="margin-right: 10px;">Companion Module</a>
        <a href="https://docs.vdo.ninja/guides/iframe-api-documentation" target="_blank">IFrame API Docs</a>
      </div>
    </div>
  </header>

  <main>
    <div class="tabs">
      <div class="tab active" data-tab="websocket">WebSocket API</div>
      <div class="tab" data-tab="http">HTTP API</div>
      <div class="tab" data-tab="sse">Server-Sent Events</div>
      <div class="tab" data-tab="post">HTTP POST</div>
      <div class="tab" data-tab="ptz">Camera Controls</div>
      <div class="tab" data-tab="midi">MIDI Integration</div>
      <div class="tab" data-tab="guest">Guest Controls</div>
      <div class="tab" data-tab="docs">API Reference</div>
    </div>

    <!-- WebSocket API Tab -->
    <div class="tab-content active" id="websocket-tab">
      <div class="section">
        <div class="section-title">WebSocket API Request Builder</div>
        <div class="request-builder">
          <div class="form-group">
            <label for="ws-action">Action:</label>
            <select id="ws-action">
              <option value="mic">mic</option>
              <option value="camera">camera</option>=
              <option value="speaker">speaker</option>
              <option value="volume">volume</option>
              <option value="record">record</option>
              <option value="bitrate">bitrate</option>
              <option value="togglehand">togglehand</option>
              <option value="reload">reload</option>
              <option value="hangup">hangup</option>
              <option value="forceKeyframe">forceKeyframe</option>
              <option value="getDetails">getDetails</option>
              <option value="soloVideo">soloVideo (highlight)</option>
              <option value="layout">layout</option>
              <option value="sendChat">sendChat</option>
              <option value="sendDirectorChat">sendDirectorChat</option>
              <option value="zoom">zoom</option>
              <option value="focus">focus</option>
              <option value="pan">pan</option>
              <option value="tilt">tilt</option>
              <option value="exposure">exposure</option>
              <option value="startRoomTimer">startRoomTimer</option>
              <option value="pauseRoomTimer">pauseRoomTimer</option>
              <option value="stopRoomTimer">stopRoomTimer</option>
            </select>
          </div>
          <div class="form-group">
            <label for="ws-value">Value:</label>
            <input type="text" id="ws-value" placeholder="true, false, toggle, or numeric value">
          </div>
          <div class="form-group">
            <label for="ws-value2">Value2 (optional):</label>
            <input type="text" id="ws-value2" placeholder="Secondary value (e.g. 'abs' for absolute zoom)">
          </div>
          <div class="form-group">
            <label for="ws-target">Target (optional - for director commands):</label>
            <input type="text" id="ws-target" placeholder="Guest slot number or stream ID">
          </div>
          <div class="form-group">
            <label for="ws-callback">Callback ID (optional):</label>
            <input type="text" id="ws-callback" placeholder="Numeric ID for tracking responses">
          </div>
          <div class="form-group">
            <label for="ws-custom-json">Custom JSON (advanced):</label>
            <textarea id="ws-custom-json" placeholder='{"action":"mic","value":true}'></textarea>
          </div>
          <div class="flex-row">
            <button onclick="sendWSRequest()" style="flex: 2;">Send WebSocket Request</button>
            <button onclick="clearWSFields()" style="flex: 1;">Clear Fields</button>
          </div>
        </div>
        
        <div class="section-title">Common Controls (WebSocket)</div>
        <div class="button-grid" id="ws-controls"></div>
        
        <div class="section-title">Layout Controls (WebSocket)</div>
        <div class="button-grid" id="ws-layouts"></div>
        
        <h3>WebSocket API Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('ws-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="ws-code-example">// Connect to WebSocket API
const socket = new WebSocket("wss://api.vdo.ninja:443");

// Track connection state
socket.onopen = function() {
  console.log("Connected to VDO.Ninja API");
  // Join with your API ID
  socket.send(JSON.stringify({ "join": "YOUR_API_ID" }));
};

// Handle messages
socket.onmessage = function(event) {
  if (event.data) {
    const data = JSON.parse(event.data);
    console.log("Received:", data);
  }
};

// Reconnection logic (crucial for production)
socket.onclose = function() {
  console.log("Connection closed, attempting to reconnect...");
  setTimeout(() => {
    // Implement reconnection logic here
  }, 1000);
};

// Example commands
function sendCommand(action, value = null, target = null) {
  const msg = { action };
  if (value !== null) msg.value = value;
  if (target !== null) msg.target = target;
  
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(msg));
  } else {
    console.error("Socket not connected");
  }
}

// Command examples:
// sendCommand("mic", "toggle");
// sendCommand("camera", false);
// sendCommand("soloVideo", "toggle");
// sendCommand("layout", 2);
// For directors - target a guest:
// sendCommand("mic", "toggle", "1"); // Target guest in slot 1</pre>
        </div>
      </div>
    </div>

    <!-- HTTP API Tab -->
    <div class="tab-content" id="http-tab">
      <div class="section">
        <div class="section-title">HTTP GET API Request Builder</div>
        <div class="request-builder">
          <div class="form-group">
            <label for="http-action">Action:</label>
            <select id="http-action">
              <option value="mic">mic</option>
              <option value="video">video</option>
              <option value="speaker">speaker</option>
              <option value="volume">volume</option>
              <option value="record">record</option>
              <option value="bitrate">bitrate</option>
              <option value="reload">reload</option>
              <option value="hangup">hangup</option>
              <option value="forceKeyframe">forceKeyframe</option>
              <option value="group">group</option>
              <option value="joinGroup">joinGroup</option>
              <option value="leaveGroup">leaveGroup</option>
              <option value="viewGroup">viewGroup</option>
              <option value="joinViewGroup">joinViewGroup</option>
              <option value="leaveViewGroup">leaveViewGroup</option>
              <option value="getDetails">getDetails</option>
              <option value="nextSlide">nextSlide</option>
              <option value="prevSlide">prevSlide</option>
              <option value="zoom">zoom</option>
              <option value="focus">focus</option>
              <option value="pan">pan</option>
              <option value="tilt">tilt</option>
              <option value="exposure">exposure</option>
            </select>
          </div>
          <div class="form-group">
            <label for="http-target">Target (optional - for director commands):</label>
            <input type="text" id="http-target" placeholder="null or guest slot number or stream ID">
          </div>
          <div class="form-group">
            <label for="http-value">Value:</label>
            <input type="text" id="http-value" placeholder="true, false, toggle, or numeric value">
          </div>
          <div class="form-group">
            <label>Generated URL:</label>
            <pre id="http-url-preview" style="background-color: var(--bg-primary); padding: 8px; white-space: pre-wrap; word-break: break-all;">https://api.vdo.ninja/YOUR_API_ID/action/target/value</pre>
          </div>
          <div class="flex-row">
            <button onclick="sendHTTPRequest()" style="flex: 2;">Send HTTP GET Request</button>
            <button onclick="clearHTTPFields()" style="flex: 1;">Clear Fields</button>
          </div>
        </div>
        
        <div class="section-title">Common Controls (HTTP)</div>
        <div class="button-grid" id="http-controls"></div>
        
        <div class="section-title">Group Controls (HTTP)</div>
        <div class="button-grid" id="http-groups"></div>
        
        <h3>HTTP API Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('http-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="http-code-example">// HTTP GET API Example
function sendHTTPRequest(apiID, action, target = "null", value = "null") {
  // Construct URL
  const url = `https://api.vdo.ninja/${apiID}/${action}/${target}/${value}`;
  
  // Send request
  fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      return response.text();
    })
    .then(data => {
      // Handle response (could be text or JSON)
      try {
        const jsonData = JSON.parse(data);
        console.log("Response JSON:", jsonData);
      } catch (e) {
        console.log("Response text:", data);
      }
    })
    .catch(error => {
      console.error("Error:", error);
    });
}

// Usage examples:
// sendHTTPRequest("YOUR_API_ID", "mic", "null", "toggle");
// sendHTTPRequest("YOUR_API_ID", "camera", "null", "false");
// sendHTTPRequest("YOUR_API_ID", "joinGroup", "null", "1");

// For directors targeting a specific guest
// sendHTTPRequest("YOUR_API_ID", "mic", "1", "toggle"); // Target guest in slot 1
// sendHTTPRequest("YOUR_API_ID", "addScene", "2", "1"); // Add guest 2 to scene 1</pre>
        </div>
      </div>
    </div>

    <!-- SSE API Tab -->
    <div class="tab-content" id="sse-tab">
      <div class="section">
        <div class="section-title">Server-Sent Events (SSE) Listener</div>
        <p>SSE provides a one-way communication channel to receive events from VDO.Ninja without sending requests.</p>
        
        <div class="request-builder">
          <div class="form-group">
            <label for="sse-status">SSE Connection Status:</label>
            <div id="sse-status" style="padding: 8px; background-color: var(--bg-primary); border-radius: var(--radius);">Disconnected</div>
          </div>
          <div class="flex-row">
            <button id="sse-connect-btn" onclick="connectSSE()">Connect SSE</button>
            <button id="sse-disconnect-btn" onclick="disconnectSSE()" disabled>Disconnect SSE</button>
          </div>
        </div>
        
        <h3>SSE API Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('sse-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="sse-code-example">// Server-Sent Events (SSE) Example
function connectToSSE(apiID) {
  // Create EventSource connection
  const eventSource = new EventSource(`https://api.vdo.ninja/sse/${apiID}`);
  
  // Connection opened
  eventSource.onopen = function() {
    console.log("SSE connection established");
  };
  
  // Listen for messages
  eventSource.onmessage = function(event) {
    try {
      const data = JSON.parse(event.data);
      console.log("SSE event:", data);
      
      // Handle different types of events
      if (data.type === "state_update") {
        // Handle state updates
        updateUI(data.state);
      } else if (data.type === "guest_joined") {
        // Handle guest joining
        console.log(`Guest joined: ${data.guestID}`);
      }
      // Add more event handlers as needed
      
    } catch (e) {
      console.error("Error parsing SSE event:", e);
    }
  };
  
  // Handle errors
  eventSource.onerror = function(error) {
    console.error("SSE connection error:", error);
    
    // Close connection
    eventSource.close();
    
    // Implement reconnection logic
    setTimeout(() => {
      console.log("Attempting to reconnect SSE...");
      connectToSSE(apiID);
    }, 3000);
  };
  
  // Return EventSource instance (for later disconnection if needed)
  return eventSource;
}

// Usage:
// const sseConnection = connectToSSE("YOUR_API_ID");
// 
// // To disconnect later:
// // sseConnection.close();</pre>
        </div>
      </div>
    </div>

    <!-- HTTP POST Tab -->
    <div class="tab-content" id="post-tab">
      <div class="section">
        <div class="section-title">HTTP POST API Request Builder</div>
        <p>Although HTTP GET is the primary API method, POST can be useful for complex data or when query parameters need to be hidden.</p>
        
        <div class="request-builder">
          <div class="form-group">
            <label for="post-action">Action:</label>
            <select id="post-action">
              <option value="mic">mic</option>
              <option value="camera">camera</option>
              <option value="video">video</option>
              <option value="speaker">speaker</option>
              <option value="volume">volume</option>
              <option value="sendChat">sendChat</option>
              <option value="sendDirectorChat">sendDirectorChat</option>
              <option value="layout">layout</option>
              <option value="getDetails">getDetails</option>
              <option value="zoom">zoom</option>
              <option value="exposure">exposure</option>
            </select>
          </div>
          <div class="form-group">
            <label for="post-value">Value:</label>
            <input type="text" id="post-value" placeholder="true, false, toggle, or numeric value">
          </div>
          <div class="form-group">
            <label for="post-target">Target (optional - for director commands):</label>
            <input type="text" id="post-target" placeholder="Guest slot number or stream ID">
          </div>
          <div class="form-group">
            <label for="post-json">Request Body JSON:</label>
            <textarea id="post-json" placeholder='{"action":"mic","value":true}'></textarea>
          </div>
          <div class="flex-row">
            <button onclick="sendPOSTRequest()" style="flex: 2;">Send HTTP POST Request</button>
            <button onclick="clearPOSTFields()" style="flex: 1;">Clear Fields</button>
          </div>
        </div>
        
        <h3>HTTP POST API Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('post-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="post-code-example">// HTTP POST API Example
function sendPOSTRequest(apiID, data) {
  // Construct URL
  const url = `https://api.vdo.ninja/${apiID}`;
  
  // Configure request
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  };
  
  // Send request
  fetch(url, options)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      console.log("Response:", data);
    })
    .catch(error => {
      console.error("Error:", error);
    });
}

// Usage examples:
// sendPOSTRequest("YOUR_API_ID", {
//   action: "mic",
//   value: "toggle"
// });
//
// sendPOSTRequest("YOUR_API_ID", {
//   action: "layout",
//   value: [
//     {"x":0,"y":0,"w":50,"h":100,"slot":0},
//     {"x":50,"y":0,"w":50,"h":100,"slot":1}
//   ]
// });
//
// For directors targeting a specific guest:
// sendPOSTRequest("YOUR_API_ID", {
//   action: "sendDirectorChat",
//   target: "1", 
//   value: "You're live in 5 seconds!"
// });</pre>
        </div>
      </div>
    </div>

    <!-- PTZ Controls Tab -->
    <div class="tab-content" id="ptz-tab">
      <div class="section">
        <div class="section-title">Camera Controls (PTZ)</div>
        <p>Remote control compatible camera hardware using pan, tilt, zoom, focus, and exposure commands.</p>
        
        <div class="ptz-controls">
          <div class="control-group">
            <h3>Zoom</h3>
            <div class="button-grid">
              <button onclick="sendPTZCommand('zoom', 0.1)">Zoom In (0.1)</button>
              <button onclick="sendPTZCommand('zoom', -0.1)">Zoom Out (-0.1)</button>
            </div>
            <div class="slider-wrapper">
              <span class="slider-label">Absolute Zoom:</span>
              <div class="slider-container">
                <input type="range" min="0" max="100" value="50" class="slider" id="zoom-slider">
                <span class="slider-value" id="zoom-value">0.5</span>
              </div>
            </div>
          </div>
          
          <div class="control-group">
            <h3>Focus</h3>
            <div class="button-grid">
              <button onclick="sendPTZCommand('focus', 0.1)">Focus Far (0.1)</button>
              <button onclick="sendPTZCommand('focus', -0.1)">Focus Near (-0.1)</button>
            </div>
          </div>
          
          <div class="control-group">
            <h3>Pan & Tilt</h3>
            <div class="button-grid">
              <button onclick="sendPTZCommand('pan', -0.1)">Pan Left (-0.1)</button>
              <button onclick="sendPTZCommand('pan', 0.1)">Pan Right (0.1)</button>
              <button onclick="sendPTZCommand('tilt', 0.1)">Tilt Up (0.1)</button>
              <button onclick="sendPTZCommand('tilt', -0.1)">Tilt Down (-0.1)</button>
            </div>
          </div>
          
          <div class="control-group">
            <h3>Exposure</h3>
            <div class="slider-wrapper">
              <span class="slider-label">Exposure Level:</span>
              <div class="slider-container">
                <input type="range" min="0" max="100" value="50" class="slider" id="exposure-slider">
                <span class="slider-value" id="exposure-value">0.5</span>
              </div>
            </div>
          </div>
        </div>
        
        <h3>PTZ Controls Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('ptz-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="ptz-code-example">// Camera Control (PTZ) Example
function sendCameraCommand(apiID, action, value, value2 = null, target = null) {
  // Using WebSocket (preferred for responsive camera control)
  if (socket && socket.readyState === WebSocket.OPEN) {
    const command = { action, value };
    if (value2 !== null) command.value2 = value2;
    if (target !== null) command.target = target;
    
    socket.send(JSON.stringify(command));
  } 
  // Using HTTP as fallback
  else {
    let url = `https://api.vdo.ninja/${apiID}/${action}`;
    
    if (target !== null) {
      url += `/${target}`;
    } else {
      url += `/null`;
    }
    
    url += `/${value}`;
    
    // value2 can't be directly used in the standard HTTP API, so it's ignored here
    
    fetch(url)
      .then(response => response.text())
      .then(data => console.log("Response:", data))
      .catch(error => console.error("Error:", error));
  }
}

// Usage examples:
// Relative adjustments
// sendCameraCommand("YOUR_API_ID", "zoom", 0.1);       // Zoom in
// sendCameraCommand("YOUR_API_ID", "zoom", -0.1);      // Zoom out
// sendCameraCommand("YOUR_API_ID", "focus", 0.1);      // Focus farther
// sendCameraCommand("YOUR_API_ID", "pan", -0.1);       // Pan left
// sendCameraCommand("YOUR_API_ID", "tilt", 0.1);       // Tilt up

// Absolute adjustment (for zoom)
// sendCameraCommand("YOUR_API_ID", "zoom", 0.75, "abs");  // Set zoom to 75%

// Targeting a specific guest's camera (for directors)
// sendCameraCommand("YOUR_API_ID", "zoom", 0.1, null, "2");  // Zoom guest 2's camera in</pre>
        </div>
      </div>
    </div>

    <!-- MIDI Integration Tab -->
    <div class="tab-content" id="midi-tab">
      <div class="section">
        <div class="section-title">MIDI Integration</div>
        <p>Control VDO.Ninja using MIDI controllers with various mapping options.</p>
        
        <div class="request-builder">
          <div class="form-group">
            <label>MIDI Device Status:</label>
            <div id="midi-status" style="padding: 8px; background-color: var(--bg-primary); border-radius: var(--radius);">MIDI access not initialized</div>
          </div>
          <div class="form-group">
            <label for="midi-device">Select MIDI Device:</label>
            <select id="midi-device" disabled>
              <option value="">No devices available</option>
            </select>
          </div>
          <div class="form-group">
            <label>MIDI Monitor (last received message):</label>
            <pre id="midi-monitor" style="background-color: var(--bg-primary); padding: 8px;">No MIDI message received</pre>
          </div>
          <div class="flex-row">
            <button id="midi-connect-btn" onclick="initMIDI()">Initialize MIDI</button>
            <button id="midi-test-btn" onclick="testMIDI()" disabled>Test MIDI Connection</button>
          </div>
        </div>
        
        <h3>Default MIDI Mappings</h3>
        <p>VDO.Ninja supports several MIDI mapping schemes, which can be configured using the <code>midiHotkeys</code> parameter.</p>
        
        <div class="midi-config">
          <div class="midi-mapping">
            <div class="midi-mapping-title">Standard Mapping (midiHotkeys=1)</div>
            <div class="midi-mapping-details">
              <ul>
                <li>G3: Toggle chat</li>
                <li>A3: Toggle mic</li>
                <li>B3: Toggle video</li>
                <li>C4: Toggle screenshare</li>
                <li>D4: Hangup</li>
                <li>E4: Raise hand</li>
                <li>F4: Toggle recording</li>
                <li>G4: Director audio</li>
                <li>A4: Director hangup</li>
                <li>B4: Toggle speaker</li>
              </ul>
            </div>
          </div>
          
          <div class="midi-mapping">
            <div class="midi-mapping-title">Alternate Mapping (midiHotkeys=2)</div>
            <div class="midi-mapping-details">
              <ul>
                <li>G1: Toggle chat</li>
                <li>A1: Toggle mic</li>
                <li>B1: Toggle video</li>
                <li>C2: Toggle screenshare</li>
                <li>D2: Hangup</li>
                <li>E2: Raise hand</li>
                <li>F2: Toggle recording</li>
                <li>G2: Director audio</li>
                <li>A2: Director hangup</li>
                <li>B2: Toggle speaker</li>
              </ul>
            </div>
          </div>
          
          <div class="midi-mapping">
            <div class="midi-mapping-title">CC-Based Controls (midiHotkeys=3)</div>
            <div class="midi-mapping-details">
              <p>Uses note C1 with different velocity values:</p>
              <ul>
                <li>C1 + velocity 0: Toggle chat</li>
                <li>C1 + velocity 1: Toggle mic</li>
                <li>C1 + velocity 2: Toggle video</li>
                <li>C1 + velocity 3: Toggle screenshare</li>
                <li>C1 + velocity 4: Hangup</li>
                <li>C1 + velocity 5: Raise hand</li>
                <li>C1 + velocity 6: Toggle recording</li>
                <li>C1 + velocity 7: Director audio</li>
                <li>C1 + velocity 8: Director hangup</li>
                <li>C1 + velocity 9: Toggle speaker</li>
              </ul>
            </div>
          </div>
          
          <div class="midi-mapping">
            <div class="midi-mapping-title">PTZ MIDI Controls</div>
            <div class="midi-mapping-details">
              <p>Use MIDI to control camera:</p>
              <ul>
                <li>C5 + velocity (0-127): Zoom (absolute)</li>
                <li>D5 + velocity (0-127): Focus</li>
                <li>E5 + velocity (0-127): Pan</li>
                <li>F5 + velocity (0-127): Tilt</li>
                <li>G5 + velocity (0-127): Exposure</li>
              </ul>
            </div>
          </div>
        </div>
        
        <h3>MIDI API Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('midi-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="midi-code-example">// MIDI Integration Example
function initializeMIDI() {
  // Check if Web MIDI API is supported
  if (!navigator.requestMIDIAccess) {
    console.error("Web MIDI API not supported in this browser");
    return Promise.reject("MIDI not supported");
  }
  
  // Request MIDI access
  return navigator.requestMIDIAccess({ sysex: false })
    .then(midiAccess => {
      console.log("MIDI Access granted");
      
      // Get MIDI inputs
      const inputs = midiAccess.inputs.values();
      const devices = [];
      
      // Setup devices
      for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
        devices.push(input.value);
        setupMIDIDevice(input.value);
      }
      
      // Listen for device connection/disconnection
      midiAccess.onstatechange = function(e) {
        console.log("MIDI state change:", e.port.name, e.port.state);
      };
      
      return { midiAccess, devices };
    })
    .catch(error => {
      console.error("Failed to get MIDI access:", error);
      throw error;
    });
}

function setupMIDIDevice(inputDevice) {
  // Set up MIDI message handler
  inputDevice.onmidimessage = function(message) {
    // Extract MIDI data
    const command = message.data[0];
    const note = message.data[1];
    const velocity = message.data[2];
    
    console.log(`MIDI message - Command: ${command}, Note: ${note}, Velocity: ${velocity}`);
    
    // Handle Note On messages (144 = Note On, channel 1)
    if (command === 144 && velocity > 0) {
      handleNoteOn(note, velocity);
    }
    // Handle Note Off messages (128 = Note Off, channel 1)
    else if (command === 128 || (command === 144 && velocity === 0)) {
      handleNoteOff(note);
    }
    // Handle Control Change messages (176 = CC, channel 1)
    else if (command === 176) {
      handleControlChange(note, velocity);
    }
  };
}

function handleNoteOn(note, velocity) {
  // Convert MIDI note number to note name (e.g. 60 = C4)
  const noteName = getNoteNameFromNumber(note);
  
  // Example mapping for midiHotkeys=1
  switch (noteName) {
    case "G3":
      sendCommand("toggleChat");
      break;
    case "A3":
      sendCommand("mic", "toggle");
      break;
    case "B3":
      sendCommand("camera", "toggle");
      break;
    case "C4":
      sendCommand("togglescreenshare");
      break;
    case "D4":
      sendCommand("hangup");
      break;
    // Add more mappings as needed
  }
}

function handleNoteOff(note) {
  // Handle note off events if needed
}

function handleControlChange(cc, value) {
  // Map control change messages to actions
  switch (cc) {
    case 20: // Example CC for zoom
      const zoomValue = value / 127; // Normalize to 0-1 range
      sendCommand("zoom", zoomValue, "abs");
      break;
    case 21: // Example CC for focus
      const focusValue = (value - 64) / 64; // Normalize to -1 to 1 range
      sendCommand("focus", focusValue);
      break;
    // Add more CC mappings as needed
  }
}

// Helper function to convert MIDI note number to note name
function getNoteNameFromNumber(noteNumber) {
  const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
  const octave = Math.floor(noteNumber / 12) - 1;
  const noteName = notes[noteNumber % 12];
  return noteName + octave;
}

// Usage:
// initializeMIDI()
//   .then(midi => console.log("MIDI ready with devices:", midi.devices))
//   .catch(error => console.error("MIDI setup failed:", error));</pre>
        </div>
      </div>
    </div>

    <!-- Guest Controls Tab -->
    <div class="tab-content" id="guest-tab">
      <div class="section">
        <div class="section-title">Guest Controls (Director Mode)</div>
        <p>These controls allow directors to manage remote guests via the API.</p>
        
        <div class="request-builder">
          <div class="form-group">
            <label for="guest-slot">Target Guest:</label>
            <select id="guest-slot" class="guest-slot-select">
              <option value="1">Guest 1</option>
              <option value="2">Guest 2</option>
              <option value="3">Guest 3</option>
              <option value="4">Guest 4</option>
            </select>
          </div>
          <div class="form-group">
            <label for="guest-action">Action:</label>
            <select id="guest-action">
              <option value="mic">mic (toggle)</option>
              <option value="camera">camera (toggle)</option>
              <option value="video">video (toggle)</option>
              <option value="hangup">hangup</option>
              <option value="soloChat">soloChat</option>
              <option value="soloChatBidirectional">soloChatBidirectional</option>
              <option value="speaker">speaker (toggle)</option>
              <option value="display">display (toggle)</option>
              <option value="forceKeyframe">forceKeyframe (rainbow fix)</option>
              <option value="soloVideo">soloVideo (highlight)</option>
              <option value="addScene">addScene</option>
              <option value="muteScene">muteScene</option>
              <option value="group">group</option>
              <option value="forward">forward (transfer)</option>
              <option value="sendChat">sendChat</option>
              <option value="sendDirectorChat">sendDirectorChat</option>
              <option value="volume">volume</option>
              <option value="startRoomTimer">startRoomTimer</option>
              <option value="pauseRoomTimer">pauseRoomTimer</option>
              <option value="stopRoomTimer">stopRoomTimer</option>
              <option value="zoom">zoom</option>
              <option value="focus">focus</option>
              <option value="pan">pan</option>
              <option value="tilt">tilt</option>
              <option value="exposure">exposure</option>
              <option value="mixorder">mixorder</option>
            </select>
          </div>
          <div class="form-group">
            <label for="guest-value">Value:</label>
            <input type="text" id="guest-value" placeholder="true, false, toggle, scene number, or text">
          </div>
          <div class="form-group">
            <label for="guest-method">API Method:</label>
            <select id="guest-method">
              <option value="websocket">WebSocket</option>
              <option value="http">HTTP GET</option>
            </select>
          </div>
          <div class="flex-row">
            <button onclick="sendGuestCommand()" style="flex: 2;">Send Guest Command</button>
            <button onclick="clearGuestFields()" style="flex: 1;">Clear Fields</button>
          </div>
        </div>
        
        <div class="button-grid" id="guest-controls"></div>
        
        <div class="slider-wrapper" style="margin-top: 1rem;">
          <span class="slider-label">Guest Mic Volume:</span>
          <div class="slider-container">
            <input type="range" min="0" max="200" value="100" class="slider" id="guest-volume-slider">
            <span class="slider-value" id="guest-volume-value">100</span>
          </div>
        </div>
        
        <h3>Guest Control Code Example</h3>
        <div>
          <div class="code-header">
            <span>JavaScript</span>
            <button class="copy-btn" onclick="copyCode('guest-code-example')">Copy</button>
          </div>
          <pre class="code-block" id="guest-code-example">// Guest Control Example (Director Mode)
function controlGuest(apiID, target, action, value = null, method = "websocket") {
  if (method === "websocket") {
    // WebSocket method
    if (socket && socket.readyState === WebSocket.OPEN) {
      const msg = {
        target: target,
        action: action
      };
      
      if (value !== null) {
        msg.value = value;
      }
      
      socket.send(JSON.stringify(msg));
      console.log("Sent guest command via WebSocket:", msg);
    } else {
      console.error("WebSocket not connected");
    }
  } else if (method === "http") {
    // HTTP method
    const targetStr = target || "null";
    const valueStr = value !== null ? value : "null";
    const url = `https://api.vdo.ninja/${apiID}/${action}/${targetStr}/${valueStr}`;
    
    fetch(url)
      .then(response => response.text())
      .then(data => console.log("Guest command response:", data))
      .catch(error => console.error("Error sending guest command:", error));
  }
}

// Usage examples:
// controlGuest("YOUR_API_ID", "1", "mic", "toggle");      // Toggle mic for guest 1
// controlGuest("YOUR_API_ID", "2", "camera", false);      // Turn off camera for guest 2
// controlGuest("YOUR_API_ID", "3", "addScene", 1);        // Add guest 3 to scene 1
// controlGuest("YOUR_API_ID", "1", "sendDirectorChat", "You're live in 10 seconds");
// controlGuest("YOUR_API_ID", "2", "volume", 85);         // Set guest 2 volume to 85%
// controlGuest("YOUR_API_ID", "4", "forward", "room123"); // Transfer guest 4 to room123</pre>
        </div>
      </div>
    </div>

    <!-- API Reference Tab -->
    <div class="tab-content" id="docs-tab">
      <div class="section">
        <div class="section-title">VDO.Ninja API Reference</div>
        <p>Complete reference for all supported API commands, parameters, and response formats.</p>
        
        <h3>API Overview</h3>
        <p>The VDO.Ninja API supports multiple communication methods:</p>
        <ul>
          <li><strong>WebSocket API</strong>: Real-time bidirectional communication (preferred for most uses)</li>
          <li><strong>HTTP GET API</strong>: Simple REST-style requests, compatible with hotkey systems</li>
          <li><strong>HTTP POST API</strong>: For complex data structures like layout objects</li>
          <li><strong>Server-Sent Events (SSE)</strong>: One-way communication channel for receiving updates</li>
          <li><strong>MIDI Integration</strong>: Control via MIDI devices using various mapping schemes</li>
        </ul>
        
        <p>For integrations with BitFocus Companion, check out the <a href="https://github.com/bitfocus/companion-module-vdo-ninja" target="_blank">official Companion module</a>.</p>
        <p>For custom embeds, refer to the <a href="https://docs.vdo.ninja/guides/iframe-api-documentation" target="_blank">IFrame API Documentation</a>.</p>
        
        <h3>Command Reference</h3>
        <p>The following tables list all available commands, their parameters, and what they do.</p>
        
        <h4>Self Controls</h4>
        <table>
          <thead>
            <tr>
              <th>Action</th>
              <th>Target</th>
              <th>Value</th>
              <th>Details</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>speaker</td>
              <td>null</td>
              <td>true/false/toggle</td>
              <td>Control local speaker (audio playback)</td>
            </tr>
            <tr>
              <td>mic</td>
              <td>null</td>
              <td>true/false/toggle</td>
              <td>Control local microphone</td>
            </tr>
            <tr>
              <td>camera</td>
              <td>null</td>
              <td>true/false/toggle</td>
              <td>Control local camera (same as video)</td>
            </tr>
            <tr>
              <td>video</td>
              <td>null</td>
              <td>true/false/toggle</td>
              <td>Control local camera (alternate command)</td>
            </tr>
            <tr>
              <td>volume</td>
              <td>null</td>
              <td>true/false/0-100</td>
              <td>Control playback volume of all audio tracks (true=100%, false=0%)</td>
            </tr>
            <tr>
              <td>record</td>
              <td>null</td>
              <td>true/false</td>
              <td>Start/stop recording the local video stream</td>
            </tr>
            <tr>
              <td>bitrate</td>
              <td>null</td>
              <td>true/false/integer</td>
              <td>Control video bitrate (true=reset, false=pause, integer=kbps)</td>
            </tr>
            <tr>
              <td>panning</td>
              <td>null</td>
              <td>true/false/0-180</td>
              <td>Control stereo panning (90=center)</td>
            </tr>
            <tr>
              <td>reload</td>
              <td>null</td>
              <td>null</td>
              <td>Reload the current page</td>
            </tr>
            <tr>
              <td>hangup</td>
              <td>null</td>
              <td>null</td>
              <td>Disconnect current session</td>
            </tr>
            <tr>
              <td>togglehand</td>
              <td>null</td>
              <td>null</td>
              <td>Toggle raised hand status</td>
            </tr>
            <tr>
              <td>togglescreenshare</td>
              <td>null</td>
              <td>null</td>
              <td>Toggle screen sharing</td>
            </tr>
            <tr>
              <td>forceKeyframe</td>
              <td>null</td>
              <td>null</td>
              <td>Force keyframe ("rainbow puke fix")</td>
            </tr>
            <tr>
              <td>sendChat</td>
              <td>null</td>
              <td>string</td>
              <td>Send chat message to all connected users</td>
            </tr>
            <tr>
              <td>getDetails</td>
              <td>null</td>
              <td>null</td>
              <td>Get detailed state information</td>
            </tr>
            <tr>
              <td>soloVideo</td>
              <td>null</td>
              <td>true/false/toggle</td>
              <td>Highlight video for all guests (director only)</td>
            </tr>
          </tbody>
        </table>
        
        <h4>Layout Controls</h4>
        <table>
          <thead>
            <tr>
              <th>Action</th>
              <th>Value</th>
              <th>Details</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>layout</td>
              <td>integer/object/array</td>
              <td>Switch to layout by index (0=auto-mix, 1-8=custom layouts) or set custom layout with object</td>
            </tr>
          </tbody>
        </table>
        
        <h4>Group Controls</h4>
        <table>
          <thead>
            <tr>
              <th>Action</th>
              <th>Value</th>
              <th>Details</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>group</td>
              <td>1-8</td>
              <td>Toggle in/out of specified group</td>
            </tr>
            <tr>
              <td>joinGroup</td>
              <td>1-8</td>
              <td>Join specified group</td>
            </tr>
            <tr>
              <td>leaveGroup</td>
              <td>1-8</td>
              <td>Leave specified group</td>
            </tr>
            <tr>
              <td>viewGroup</td>
              <td>1-8</td>
              <td>Toggle preview of specified group</td>
            </tr>
            <tr>
              <td>joinViewGroup</td>
              <td>1-8</td>
              <td>Preview specified group</td>
            </tr>
            <tr>
              <td>leaveViewGroup</td>
              <td>1-8</td>
              <td>Stop previewing specified group</td>
            </tr>
          </tbody>
        </table>
        
        <h4>Camera Control (PTZ)</h4>
        <table>
          <thead>
            <tr>
              <th>Action</th>
              <th>Value</th>
              <th>Value2</th>
              <th>Details</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>zoom</td>
              <td>decimal value</td>
              <td>abs/true (optional)</td>
              <td>Adjust zoom (positive=in, negative=out, or absolute with Value2)</td>
            </tr>
            <tr>
              <td>focus</td>
              <td>decimal value</td>
              <td>n/a</td>
              <td>Adjust focus (positive=far, negative=near)</td>
            </tr>
            <tr>
              <td>pan</td>
              <td>decimal value</td>
              <td>n/a</td>
              <td>Adjust pan (positive=right, negative=left)</td>
            </tr>
            <tr>
              <td>tilt</td>
              <td>decimal value</td>
              <td>n/a</td>
              <td>Adjust tilt (positive=up, negative=down)</td>
            </tr>
            <tr>
              <td>exposure</td>
              <td>0-1</td>
              <td>n/a</td>
              <td>Set exposure level (0=dark, 1=bright)</td>
            </tr>
          </tbody>
        </table>
        
        <h4>Guest Controls (Director Mode)</h4>
        <table>
          <thead>
            <tr>
              <th>Action</th>
              <th>Target</th>
              <th>Value</th>
              <th>Details</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>forward</td>
              <td>guest slot/ID</td>
              <td>room name</td>
              <td>Transfer guest to specified room</td>
            </tr>
            <tr>
              <td>addScene</td>
              <td>guest slot/ID</td>
              <td>0-8 or scene name</td>
              <td>Toggle guest in/out of specified scene</td>
            </tr>
            <tr>
              <td>muteScene</td>
              <td>guest slot/ID</td>
              <td>scene (optional)</td>
              <td>Toggle guest's mic in scenes</td>
            </tr>
            <tr>
              <td>mic</td>
              <td>guest slot/ID</td>
              <td>true/false/toggle</td>
              <td>Control guest's microphone</td>
            </tr>
            <tr>
              <td>camera</td>
              <td>guest slot/ID</td>
              <td>true/false/toggle</td>
              <td>Control guest's camera</td>
            </tr>
            <tr>
              <td>video</td>
              <td>guest slot/ID</td>
              <td>true/false/toggle</td>
              <td>Control guest's camera (alternate command)</td>
            </tr>
            <tr>
              <td>hangup</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Disconnect guest</td>
            </tr>
            <tr>
              <td>soloChat</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Toggle one-way solo chat with guest</td>
            </tr>
            <tr>
              <td>soloChatBidirectional</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Toggle two-way solo chat with guest</td>
            </tr>
            <tr>
              <td>speaker</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Toggle guest's speaker</td>
            </tr>
            <tr>
              <td>display</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Toggle guest's display (ability to see video)</td>
            </tr>
            <tr>
              <td>group</td>
              <td>guest slot/ID</td>
              <td>1-8</td>
              <td>Toggle guest in/out of specified group</td>
            </tr>
            <tr>
              <td>forceKeyframe</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Trigger keyframe for guest (fix rainbow artifacts)</td>
            </tr>
            <tr>
              <td>soloVideo</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Toggle highlighting guest's video</td>
            </tr>
            <tr>
              <td>sendChat</td>
              <td>guest slot/ID</td>
              <td>message text</td>
              <td>Send private chat message to guest</td>
            </tr>
            <tr>
              <td>sendDirectorChat</td>
              <td>guest slot/ID</td>
              <td>message text</td>
              <td>Send message and overlay it on guest's screen</td>
            </tr>
            <tr>
              <td>volume</td>
              <td>guest slot/ID</td>
              <td>0-200</td>
              <td>Set guest's microphone volume</td>
            </tr>
            <tr>
              <td>startRoomTimer</td>
              <td>guest slot/ID</td>
              <td>seconds</td>
              <td>Start countdown timer for guest</td>
            </tr>
            <tr>
              <td>pauseRoomTimer</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Pause timer for guest</td>
            </tr>
            <tr>
              <td>stopRoomTimer</td>
              <td>guest slot/ID</td>
              <td>null</td>
              <td>Stop timer for guest</td>
            </tr>
            <tr>
              <td>mixorder</td>
              <td>guest slot/ID</td>
              <td>-1 or 1</td>
              <td>Adjust guest's position in the mixer (-1=up, 1=down)</td>
            </tr>
          </tbody>
        </table>
        
        <h3>Response Formats</h3>
        <p>Responses from the API depend on the method used:</p>
        <ul>
          <li><strong>HTTP GET</strong>: Responses include simple text values like <code>true</code>, <code>false</code>, <code>null</code>, <code>fail</code>, or a specific value. For object responses like <code>getDetails</code>, you'll receive JSON.</li>
          <li><strong>WebSocket</strong>: Responses are JSON objects containing the result along with the original request parameters and any custom data fields you included.</li>
          <li><strong>HTTP POST</strong>: Responses are JSON objects with similar structure to WebSocket responses.</li>
          <li><strong>SSE</strong>: Events are JSON objects containing state updates and other notifications.</li>
        </ul>
        
        <h3>Custom Layout Format</h3>
        <p>When using the <code>layout</code> command, you can provide a custom layout object or array:</p>
        <div>
          <div class="code-header">
            <span>Layout Object Example</span>
            <button class="copy-btn" onclick="copyCode('layout-example')">Copy</button>
          </div>
          <pre class="code-block" id="layout-example">// Single layout example
{
  "action": "layout",
  "value": [
    {"x": 0, "y": 0, "w": 50, "h": 100, "slot": 0},
    {"x": 50, "y": 0, "w": 50, "h": 100, "slot": 1}
  ]
}

// Using URL parameter to define multiple layouts
// ?layouts=[[{"x":0,"y":0,"w":100,"h":100,"slot":0}],[{"x":0,"y":0,"w":50,"h":100,"slot":0},{"x":50,"y":0,"w":50,"h":100,"slot":1}]]

// Layout properties:
// x, y: Position (percentage of container)
// w, h: Width and height (percentage of container)
// slot: Guest slot number (0-indexed)
// c: Crop to fit (true/false)
// z: Z-index (layer)

// To switch layouts:
// {"action": "layout", "value": 0} // Auto-mix
// {"action": "layout", "value": 1} // First custom layout
// {"action": "layout", "value": 2} // Second custom layout</pre>
        </div>
      </div>
    </div>
  </main>

  <div class="logger-container">
    <div class="api-logger-header">
      <span><strong>API Request/Response Logger</strong></span>
      <button class="copy-btn" onclick="clearLogs()">Clear</button>
    </div>
    <div class="api-logger" id="api-logger"></div>
  </div>
</div>

<script>
// Utility functions
function generateStreamID() {
  const possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
  let text = "";
  for (let i = 0; i < 10; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

// Copy to clipboard
function copyToClipboard(element) {
  const text = element.textContent;
  navigator.clipboard.writeText(text).then(() => {
    // Show tooltip
    const tooltip = document.createElement('div');
    tooltip.className = 'tooltip-text';
    tooltip.style = 'position: fixed; background: #333; color: white; padding: 5px 10px; border-radius: 3px; z-index: 100; opacity: 0; transition: opacity 0.3s;';
    tooltip.textContent = 'Copied to clipboard!';
    
    element.appendChild(tooltip);
    setTimeout(() => {
      tooltip.style.opacity = '1';
    }, 10);
    
    setTimeout(() => {
      tooltip.style.opacity = '0';
      setTimeout(() => {
        element.removeChild(tooltip);
      }, 300);
    }, 2000);
  });
}

// Copy code example
function copyCode(elementId) {
  const code = document.getElementById(elementId).textContent;
  navigator.clipboard.writeText(code).then(() => {
    const btn = event.target;
    const originalText = btn.textContent;
    btn.textContent = 'Copied!';
    setTimeout(() => {
      btn.textContent = originalText;
    }, 2000);
  });
}

// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
  tab.addEventListener('click', () => {
    document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
    
    tab.classList.add('active');
    document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active');
  });
});

// Sliders
document.getElementById('zoom-slider').addEventListener('input', function() {
  const value = this.value / 100;
  document.getElementById('zoom-value').textContent = value.toFixed(2);
});

document.getElementById('zoom-slider').addEventListener('change', function() {
  const value = this.value / 100;
  sendPTZCommand('zoom', value, 'abs');
});

document.getElementById('exposure-slider').addEventListener('input', function() {
  const value = this.value / 100;
  document.getElementById('exposure-value').textContent = value.toFixed(2);
});

document.getElementById('exposure-slider').addEventListener('change', function() {
  const value = this.value / 100;
  sendPTZCommand('exposure', value);
});

document.getElementById('guest-volume-slider').addEventListener('input', function() {
  document.getElementById('guest-volume-value').textContent = this.value;
});

document.getElementById('guest-volume-slider').addEventListener('change', function() {
  const guestSlot = document.getElementById('guest-slot').value;
  sendGuestCommand(guestSlot, "volume", parseInt(this.value));
});

// HTTP URL preview
function updateHTTPUrlPreview() {
  const action = document.getElementById('http-action').value;
  const target = document.getElementById('http-target').value || "null";
  const value = document.getElementById('http-value').value || "null";
  const apiID = WID;
  
  document.getElementById('http-url-preview').textContent = `https://api.vdo.ninja/${apiID}/${action}/${target}/${value}`;
}

document.getElementById('http-action').addEventListener('change', updateHTTPUrlPreview);
document.getElementById('http-target').addEventListener('input', updateHTTPUrlPreview);
document.getElementById('http-value').addEventListener('input', updateHTTPUrlPreview);

// Form field clearing
function clearWSFields() {
  document.getElementById('ws-value').value = '';
  document.getElementById('ws-target').value = '';
  document.getElementById('ws-value2').value = '';
  document.getElementById('ws-callback').value = '';
  document.getElementById('ws-custom-json').value = '';
}

function clearHTTPFields() {
  document.getElementById('http-target').value = '';
  document.getElementById('http-value').value = '';
  updateHTTPUrlPreview();
}

function clearPOSTFields() {
  document.getElementById('post-value').value = '';
  document.getElementById('post-target').value = '';
  
  // Reset JSON to template based on action
  const action = document.getElementById('post-action').value;
  let template = { action };
  document.getElementById('post-json').value = JSON.stringify(template, null, 2);
}

function clearGuestFields() {
  document.getElementById('guest-value').value = '';
}

// Logging functions
function clearLogs() {
  document.getElementById('api-logger').innerHTML = '';
}

function addLogEntry(type, method, endpoint, data, response) {
  const logger = document.getElementById('api-logger');
  const logEntry = document.createElement('div');
  logEntry.className = 'log-entry';
  
  const time = new Date().toLocaleTimeString();
  
  let html = `<span class="log-time">[${time}]</span>`;
  html += `<span class="log-method log-${method.toLowerCase()}">${method}</span>`;
  
  if (type === 'request') {
    html += `<span class="log-request">${endpoint}</span>`;
    if (data) {
      html += `<br><span class="log-request">Data: ${typeof data === 'object' ? JSON.stringify(data) : data}</span>`;
    }
  } else if (type === 'response') {
    html += `<span class="log-response">Response: ${typeof response === 'object' ? JSON.stringify(response) : response}</span>`;
  } else if (type === 'error') {
    html += `<span class="log-error">Error: ${data}</span>`;
  }
  
  logEntry.innerHTML = html;
  logger.appendChild(logEntry);
  logger.scrollTop = logger.scrollHeight;
}

// Parse URL parameters
(function (w) {
  w.URLSearchParams = w.URLSearchParams || function (searchString) {
    var self = this;
    self.searchString = searchString;
    self.get = function (name) {
      var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
      if (results == null) {
        return null;
      } else {
        return decodeURI(results[1]) || 0;
      }
    };
  };
})(window);

const urlParams = new URLSearchParams(window.location.search);
let WID = "testOBSN";

// Handle API ID
if (urlParams.has("api")) {
  WID = urlParams.get("api");
} else if (urlParams.has("osc")) {
  WID = urlParams.get("osc");
} else if (urlParams.has("id")) {
  WID = urlParams.get("id");
} else if (urlParams.has("ID")) {
  WID = urlParams.get("ID");
} else if (urlParams.has("wid")) {
  WID = urlParams.get("wid");
} else {
  WID = generateStreamID();
  
  const href = window.location.href;
  const arr = href.split('?');
  let newurl;
  if (arr.length > 1 && arr[1] !== '') {
    newurl = href + '&api=' + WID;
  } else {
    newurl = href + '?api=' + WID;
  }

  window.history.pushState({ path: newurl.toString() }, '', newurl.toString());
}

// Update the displayed API key
document.getElementById('api-key').textContent = WID;

// Create the VDO.Ninja link
const ninjaPath = "vdo.ninja";
const ninjaLink = document.getElementById('ninja-link');
ninjaLink.href = `https://${ninjaPath}/?api=${WID}`;
ninjaLink.textContent = `https://${ninjaPath}/?api=${WID}`;

// WebSocket setup
let socket = null;
let connecting = false;
let failedCount = 0;
let callbackID = parseInt(Math.random() * 1000);

function connect() {
  clearTimeout(connecting);
  if (socket) {
    if (socket.readyState === socket.OPEN) {
      updateConnectionStatus('connected');
      return;
    }
    try {
      socket.close();
    } catch (e) { }
  }
  
  updateConnectionStatus('connecting');
  socket = new WebSocket("wss://api.vdo.ninja:443");
  
  socket.onclose = function() {
    updateConnectionStatus('disconnected');
    failedCount += 1;
    clearTimeout(connecting);
    connecting = setTimeout(function() {
      connect();
    }, 100 * (failedCount - 1));
  };

  socket.onerror = function() {
    updateConnectionStatus('disconnected');
    failedCount += 1;
    clearTimeout(connecting);
    connecting = setTimeout(function() {
      connect();
    }, 100 * failedCount);
  };

  socket.onopen = function() {
    updateConnectionStatus('connected');
    failedCount = 0;
    try {
      socket.send(JSON.stringify({ "join": WID }));
      addLogEntry('request', 'WS', 'Connection', { "join": WID });
    } catch (e) {
      connecting = setTimeout(function() {
        connect();
      }, 1);
    }
  };
  
  socket.addEventListener('message', function(event) {
    if (event.data) {
      const data = JSON.parse(event.data);
      addLogEntry('response', 'WS', 'Message', null, data);
      
      if ("callback" in data) {
        if (data.callback.value == callbackID) {
          console.log("Received callback response:", data);
        }
      }
    }
  });
}

function updateConnectionStatus(status) {
  const indicator = document.getElementById('connection-indicator');
  const statusText = document.getElementById('connection-status');
  
  indicator.className = 'status-indicator';
  
  if (status === 'connected') {
    indicator.classList.add('status-connected');
    statusText.textContent = 'Connected';
    statusText.style.color = 'var(--accent-success)';
  } else if (status === 'disconnected') {
    indicator.classList.add('status-disconnected');
    statusText.textContent = 'Disconnected';
    statusText.style.color = 'var(--accent-danger)';
  } else {
    indicator.classList.add('status-connecting');
    statusText.textContent = 'Connecting...';
    statusText.style.color = 'var(--accent-warning)';
  }
}

// Connect to the WebSocket server
connect();

// Send messages via WebSocket
function sendMessage(msg) {
  if (!socket || socket.readyState !== socket.OPEN) {
    addLogEntry('error', 'WS', 'Not connected', "WebSocket not connected");
    connect();
    return;
  }
  
  try {
    if (typeof msg === 'object') {
      msg = JSON.stringify(msg);
    }
    socket.send(msg);
    addLogEntry('request', 'WS', 'Send Message', JSON.parse(msg));
  } catch (e) {
    addLogEntry('error', 'WS', 'Send Error', e.message);
    connecting = setTimeout(function() {
      connect();
    }, 100);
  }
}

// Send WebSocket request based on form
function sendWSRequest() {
  try {
    const customJson = document.getElementById('ws-custom-json').value.trim();
    
    if (customJson) {
      // Send custom JSON
      const jsonObj = JSON.parse(customJson);
      sendMessage(jsonObj);
    } else {
      // Build from form fields
      const action = document.getElementById('ws-action').value;
      const value = document.getElementById('ws-value').value;
      const value2 = document.getElementById('ws-value2').value;
      const target = document.getElementById('ws-target').value;
      const callback = document.getElementById('ws-callback').value;
      
      const msg = { action };
      
      // Parse value if needed
      if (value) {
        if (value === 'true') msg.value = true;
        else if (value === 'false') msg.value = false;
        else if (!isNaN(value)) msg.value = Number(value);
        else msg.value = value;
      }
      
      // Add optional params
      if (value2) msg.value2 = value2;
      if (target) msg.target = target;
      if (callback) msg.cid = callback;
      
      sendMessage(msg);
    }
  } catch (e) {
    addLogEntry('error', 'WS', 'Invalid Request', e.message);
  }
}

// Send HTTP GET request
function sendHTTPRequest() {
  try {
    const action = document.getElementById('http-action').value;
    const target = document.getElementById('http-target').value || "null";
    const value = document.getElementById('http-value').value || "null";
    
    const url = `https://api.vdo.ninja/${WID}/${action}/${target}/${value}`;
    
    addLogEntry('request', 'GET', url);
    
    fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.text();
      })
      .then(data => {
        // Try to parse as JSON, fall back to text
        try {
          const jsonData = JSON.parse(data);
          addLogEntry('response', 'GET', url, null, jsonData);
        } catch (e) {
          addLogEntry('response', 'GET', url, null, data);
        }
      })
      .catch(error => {
        addLogEntry('error', 'GET', url, error.message);
      });
  } catch (e) {
    addLogEntry('error', 'GET', 'Invalid Request', e.message);
  }
}

// Send HTTP POST request
function sendPOSTRequest() {
  try {
    const customJson = document.getElementById('post-json').value.trim();
    let jsonData;
    
    if (customJson) {
      jsonData = JSON.parse(customJson);
    } else {
      const action = document.getElementById('post-action').value;
      const value = document.getElementById('post-value').value;
      const target = document.getElementById('post-target').value;
      
      jsonData = { action };
      
      // Parse value if needed
      if (value) {
        if (value === 'true') jsonData.value = true;
        else if (value === 'false') jsonData.value = false;
        else if (!isNaN(value)) jsonData.value = Number(value);
        else jsonData.value = value;
      }
      
      // Add target if specified
      if (target) jsonData.target = target;
    }
    
    const url = `https://api.vdo.ninja/${WID}`;
    
    addLogEntry('request', 'POST', url, jsonData);
    
    fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(jsonData)
    })
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      addLogEntry('response', 'POST', url, null, data);
    })
    .catch(error => {
      addLogEntry('error', 'POST', url, error.message);
    });
  } catch (e) {
    addLogEntry('error', 'POST', 'Invalid Request', e.message);
  }
}

// SSE Connection
let eventSource = null;

function connectSSE() {
  if (eventSource) {
    disconnectSSE();
  }
  
  const sseStatus = document.getElementById('sse-status');
  sseStatus.textContent = 'Connecting...';
  sseStatus.style.color = 'var(--accent-warning)';
  
  eventSource = new EventSource(`https://api.vdo.ninja/sse/${WID}`);
  
  addLogEntry('request', 'SSE', `https://api.vdo.ninja/sse/${WID}`);
  
  eventSource.onopen = function() {
    sseStatus.textContent = 'Connected - Listening for events';
    sseStatus.style.color = 'var(--accent-success)';
    
    document.getElementById('sse-connect-btn').disabled = true;
    document.getElementById('sse-disconnect-btn').disabled = false;
    
    addLogEntry('response', 'SSE', 'Connection established');
  };
  
  eventSource.onmessage = function(event) {
    try {
      const data = JSON.parse(event.data);
      addLogEntry('response', 'SSE', 'Event received', null, data);
    } catch (e) {
      addLogEntry('error', 'SSE', 'Parse Error', e.message);
    }
  };
  
  eventSource.onerror = function(error) {
    sseStatus.textContent = 'Error - Connection closed';
    sseStatus.style.color = 'var(--accent-danger)';
    
    document.getElementById('sse-connect-btn').disabled = false;
    document.getElementById('sse-disconnect-btn').disabled = true;
    
    addLogEntry('error', 'SSE', 'Connection Error', error);
    eventSource.close();
    eventSource = null;
  };
}

function disconnectSSE() {
  if (eventSource) {
    eventSource.close();
    eventSource = null;
    
    const sseStatus = document.getElementById('sse-status');
    sseStatus.textContent = 'Disconnected';
    sseStatus.style.color = 'var(--text-secondary)';
    
    document.getElementById('sse-connect-btn').disabled = false;
    document.getElementById('sse-disconnect-btn').disabled = true;
    
    addLogEntry('request', 'SSE', 'Connection closed manually');
  }
}

// PTZ camera controls
function sendPTZCommand(action, value, value2 = null) {
  const command = { action, value };
  if (value2) command.value2 = value2;
  
  sendMessage(command);
}

// Guest control commands
function sendGuestCommand(target, action, value = null) {
  if (!target) {
    target = document.getElementById('guest-slot').value;
  }
  if (!action) {
    action = document.getElementById('guest-action').value;
  }
  if (value === undefined && document.getElementById('guest-value').value) {
    const rawValue = document.getElementById('guest-value').value;
    
    // Parse value appropriately
    if (rawValue === 'true') value = true;
    else if (rawValue === 'false') value = false;
    else if (rawValue === 'toggle') value = 'toggle';
    else if (!isNaN(rawValue)) value = Number(rawValue);
    else value = rawValue;
  }
  
  const method = document.getElementById('guest-method').value;
  
  if (method === 'websocket') {
    // WebSocket method
    const msg = { target, action };
    if (value !== null) msg.value = value;
    
    sendMessage(msg);
  } else if (method === 'http') {
    // HTTP method
    const targetStr = target || "null";
    const valueStr = value !== null ? value : "null";
    const url = `https://api.vdo.ninja/${WID}/${action}/${targetStr}/${valueStr}`;
    
    addLogEntry('request', 'GET', url);
    
    fetch(url)
      .then(response => response.text())
      .then(data => {
        try {
          const jsonData = JSON.parse(data);
          addLogEntry('response', 'GET', url, null, jsonData);
        } catch (e) {
          addLogEntry('response', 'GET', url, null, data);
        }
      })
      .catch(error => {
        addLogEntry('error', 'GET', url, error.message);
      });
  }
}

// MIDI integration
let midiAccess = null;
let activeInputs = [];

function initMIDI() {
  if (!navigator.requestMIDIAccess) {
    const midiStatus = document.getElementById('midi-status');
    midiStatus.textContent = 'Web MIDI API not supported in this browser';
    midiStatus.style.color = 'var(--accent-danger)';
    addLogEntry('error', 'MIDI', 'MIDI not supported', 'Web MIDI API not available in this browser');
    return;
  }
  
  navigator.requestMIDIAccess({ sysex: false })
    .then(access => {
      midiAccess = access;
      updateMIDIStatus();
      populateMIDIDevices();
      
      midiAccess.onstatechange = function(e) {
        updateMIDIStatus();
        populateMIDIDevices();
      };
      
      document.getElementById('midi-test-btn').disabled = false;
      addLogEntry('response', 'MIDI', 'MIDI access granted');
    })
    .catch(error => {
      const midiStatus = document.getElementById('midi-status');
      midiStatus.textContent = `Error: ${error.message}`;
      midiStatus.style.color = 'var(--accent-danger)';
      addLogEntry('error', 'MIDI', 'MIDI access denied', error.message);
    });
}

function updateMIDIStatus() {
  if (!midiAccess) return;
  
  const midiStatus = document.getElementById('midi-status');
  const inputs = Array.from(midiAccess.inputs.values());
  const outputs = Array.from(midiAccess.outputs.values());
  
  midiStatus.textContent = `Available: ${inputs.length} input(s), ${outputs.length} output(s)`;
  midiStatus.style.color = inputs.length > 0 ? 'var(--accent-success)' : 'var(--text-secondary)';
}

function populateMIDIDevices() {
  if (!midiAccess) return;
  
  const deviceSelect = document.getElementById('midi-device');
  deviceSelect.innerHTML = '';
  deviceSelect.disabled = false;
  
  // Add placeholder option
  const noneOption = document.createElement('option');
  noneOption.value = '';
  noneOption.textContent = '-- Select MIDI device --';
  deviceSelect.appendChild(noneOption);
  
  // Add all inputs
  let anyDevices = false;
  midiAccess.inputs.forEach(input => {
    anyDevices = true;
    const option = document.createElement('option');
    option.value = input.id;
    option.textContent = input.name || `MIDI Input ${input.id}`;
    deviceSelect.appendChild(option);
  });
  
  // Set the select status appropriately
  if (!anyDevices) {
    const noDevicesOption = document.createElement('option');
    noDevicesOption.value = '';
    noDevicesOption.textContent = 'No MIDI devices available';
    deviceSelect.innerHTML = '';
    deviceSelect.appendChild(noDevicesOption);
    deviceSelect.disabled = true;
  }
  
  // Setup device change handler
  deviceSelect.onchange = function() {
    setupMIDIDevice(this.value);
  };
}

function setupMIDIDevice(deviceId) {
  // Clear all existing MIDI input handlers
  activeInputs.forEach(input => {
    input.onmidimessage = null;
  });
  activeInputs = [];
  
  if (!deviceId) return;
  
  const input = midiAccess.inputs.get(deviceId);
  if (!input) return;
  
  // Setup new handler
  input.onmidimessage = handleMIDIMessage;
  activeInputs.push(input);
  
  addLogEntry('request', 'MIDI', `Connected to ${input.name || deviceId}`);
}

function handleMIDIMessage(message) {
  const monitor = document.getElementById('midi-monitor');
  const command = message.data[0];
  const note = message.data[1];
  const velocity = message.data[2];
  
  const commandType = 
    command >= 144 && command <= 159 ? 'Note On' :
    command >= 128 && command <= 143 ? 'Note Off' :
    command >= 176 && command <= 191 ? 'Control Change' :
    'Other';
  
  // Display MIDI message
  monitor.textContent = `Command: ${command} (${commandType}), Note/CC: ${note}, Velocity/Value: ${velocity}`;
  
  // Log the MIDI message
  const noteOrCC = commandType === 'Control Change' ? `CC ${note}` : getNoteNameFromNumber(note);
  addLogEntry('request', 'MIDI', `${commandType} ${noteOrCC} ${velocity}`);
  
  // Process MIDI mapping (basic example)
  if (commandType === 'Note On' && velocity > 0) {
    processMIDICommand(note, velocity);
  }
}

function processMIDICommand(note, velocity) {
  const noteName = getNoteNameFromNumber(note);

  // Example mapping for midiHotkeys=1
  if (noteName === 'G3') {
	sendMessage({ action: 'toggleChat' });
  } else if (noteName === 'A3') {
	sendMessage({ action: 'mic', value: 'toggle' });
  } else if (noteName === 'B3') {
	sendMessage({ action: 'camera', value: 'toggle' });
  }

  // Process camera controls (C5-G5 notes)
  if (noteName === 'C5') {
	const zoomValue = velocity / 127; // Normalize to 0-1
	sendPTZCommand('zoom', zoomValue, 'abs');
  } else if (noteName === 'D5') { /* Add this */
	const focusValue = velocity / 127; // Normalize to 0-1
	sendPTZCommand('focus', focusValue);
  } else if (noteName === 'E5') { /* Add this */
	const panValue = (velocity - 64) / 64; // Normalize to -1 to 1
	sendPTZCommand('pan', panValue);
  } else if (noteName === 'F5') { /* Add this */
	const tiltValue = (velocity - 64) / 64; // Normalize to -1 to 1
	sendPTZCommand('tilt', tiltValue);
  } else if (noteName === 'G5') { /* Add this */
	const exposureValue = velocity / 127; // Normalize to 0-1
	sendPTZCommand('exposure', exposureValue);
  }
}

function testMIDI() {
  // Send a test MIDI message to the console
  addLogEntry('request', 'MIDI', 'Test message sent');
  
  // Simulate a MIDI message
  const monitor = document.getElementById('midi-monitor');
  monitor.textContent = 'Test MIDI message: Note On C4 (60) velocity 127';
}

// Helper function to convert MIDI note number to note name
function getNoteNameFromNumber(noteNumber) {
  const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  const octave = Math.floor(noteNumber / 12) - 1;
  const noteName = notes[noteNumber % 12];
  return noteName + octave;
}

// Create control buttons
function createWebSocketControls() {
  const container = document.getElementById('ws-controls');
  container.innerHTML = '';
  
  // Define control buttons
  const controls = [
    { name: "Mic On", action: "mic", value: true, type: "true" },
    { name: "Mic Off", action: "mic", value: false, type: "false" },
    { name: "Mic Toggle", action: "mic", value: "toggle", type: "toggle" },
    
    { name: "Camera On", action: "camera", value: true, type: "true" },
    { name: "Camera Off", action: "camera", value: false, type: "false" },
    { name: "Camera Toggle", action: "camera", value: "toggle", type: "toggle" },
    
    { name: "Video On", action: "video", value: true, type: "true" },
    { name: "Video Off", action: "video", value: false, type: "false" },
    { name: "Video Toggle", action: "video", value: "toggle", type: "toggle" },
    
    { name: "Speaker On", action: "speaker", value: true, type: "true" },
    { name: "Speaker Off", action: "speaker", value: false, type: "false" },
    { name: "Speaker Toggle", action: "speaker", value: "toggle", type: "toggle" },
    
    { name: "Volume 100%", action: "volume", value: 100, type: "true" },
    { name: "Volume 0%", action: "volume", value: 0, type: "false" },
    { name: "Volume 50%", action: "volume", value: 50, type: "toggle" },
    
    { name: "Record Start", action: "record", value: true, type: "true" },
    { name: "Record Stop", action: "record", value: false, type: "false" },
    
    { name: "Bitrate Reset", action: "bitrate", value: true, type: "true" },
    { name: "Bitrate 0 (Pause)", action: "bitrate", value: false, type: "false" },
    { name: "Bitrate 1000kb", action: "bitrate", value: 1000, type: "toggle" },
    
    { name: "Toggle Hand", action: "togglehand", value: null, type: "toggle" },
    { name: "Reload Page", action: "reload", value: true, type: "toggle" },
    { name: "Hangup", action: "hangup", value: true, type: "false" },
    
    { name: "Force Keyframe", action: "forceKeyframe", value: null, type: "toggle" },
    { name: "Say Hello", action: "sendChat", value: "Hello", type: "toggle" },
    { name: "Overlay Hello", action: "sendDirectorChat", value: "Hello", type: "toggle" },
    
    { name: "Get Details", action: "getDetails", value: null, type: "toggle" },
    { name: "Highlight Toggle", action: "soloVideo", value: "toggle", type: "toggle" },
  ];
  
  // Create and add buttons to the container
  controls.forEach(control => {
    const button = document.createElement('button');
    button.textContent = control.name;
    button.setAttribute('data-type', control.type);
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    
    // Format the request info based on the control
    let apiFormat = `WS: {"action":"${control.action}"`;
    if (control.value !== null) {
      apiFormat += `,"value":${typeof control.value === 'string' ? `"${control.value}"` : control.value}`;
    }
    apiFormat += "}";
    
    tooltip.textContent = apiFormat;
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    // Set onclick handler
    button.onclick = function() {
      const msg = { action: control.action };
      if (control.value !== null) msg.value = control.value;
      sendMessage(JSON.stringify(msg));
    };
    
    container.appendChild(button);
  });
}

// Create layout control buttons
function createLayoutControls() {
  const container = document.getElementById('ws-layouts');
  container.innerHTML = '';
  
  // Create buttons for layouts 0-8
  for (let i = 0; i <= 8; i++) {
    const button = document.createElement('button');
    const name = i === 0 ? "Auto-Mix" : `Layout ${i}`;
    button.textContent = name;
    button.setAttribute('data-type', "toggle");
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    tooltip.textContent = `WS: {"action":"layout","value":${i}}`;
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    // Set onclick handler
    button.onclick = function() {
      sendMessage(JSON.stringify({ "action": "layout", "value": i }));
    };
    
    container.appendChild(button);
  }
  
  // Add a few sample grid layouts
  const sampleLayouts = [
    {
      name: "50/50 Split",
      layout: [
        {"x":0,"y":0,"w":50,"h":100,"slot":0},
        {"x":50,"y":0,"w":50,"h":100,"slot":1}
      ]
    },
    {
      name: "Picture in Picture",
      layout: [
        {"x":0,"y":0,"w":100,"h":100,"slot":0},
        {"x":70,"y":70,"w":30,"h":30,"z":1,"c":true,"slot":1}
      ]
    },
    {
      name: "2x2 Grid",
      layout: [
        {"x":0,"y":0,"w":50,"h":50,"slot":0},
        {"x":50,"y":0,"w":50,"h":50,"slot":1},
        {"x":0,"y":50,"w":50,"h":50,"slot":2},
        {"x":50,"y":50,"w":50,"h":50,"slot":3}
      ]
    }
  ];
  
  sampleLayouts.forEach(sample => {
    const button = document.createElement('button');
    button.textContent = sample.name;
    button.setAttribute('data-type', "toggle");
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    tooltip.textContent = `WS: {"action":"layout","value":${JSON.stringify(sample.layout)}}`;
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    // Set onclick handler
    button.onclick = function() {
      sendMessage(JSON.stringify({ "action": "layout", "value": sample.layout }));
    };
    
    container.appendChild(button);
  });
}

// Create HTTP API buttons
function createHTTPControls() {
  const container = document.getElementById('http-controls');
  container.innerHTML = '';
  
  // Define HTTP API buttons
  const controls = [
    { name: "Mic On", action: "mic", value: true, type: "true" },
    { name: "Mic Off", action: "mic", value: false, type: "false" },
    { name: "Mic Toggle", action: "mic", value: "toggle", type: "toggle" },
    
    { name: "Camera On", action: "camera", value: true, type: "true" },
    { name: "Camera Off", action: "camera", value: false, type: "false" },
    { name: "Camera Toggle", action: "camera", value: "toggle", type: "toggle" },
    
    { name: "Video On", action: "video", value: true, type: "true" },
    { name: "Video Off", action: "video", value: false, type: "false" },
    { name: "Video Toggle", action: "video", value: "toggle", type: "toggle" },
    
    { name: "Speaker On", action: "speaker", value: true, type: "true" },
    { name: "Speaker Off", action: "speaker", value: false, type: "false" },
    { name: "Speaker Toggle", action: "speaker", value: "toggle", type: "toggle" },
    
    { name: "Volume 100%", action: "volume", value: 100, type: "true" },
	{ name: "Volume 0%", action: "volume", value: 0, type: "false" },
    { name: "Volume 50%", action: "volume", value: 50, type: "toggle" },
    
    { name: "Record Start", action: "record", value: true, type: "true" },
    { name: "Record Stop", action: "record", value: false, type: "false" },
    
    { name: "Bitrate Reset", action: "bitrate", value: true, type: "true" },
    { name: "Bitrate 0 (Pause)", action: "bitrate", value: false, type: "false" },
    { name: "Bitrate 1000kb", action: "bitrate", value: 1000, type: "toggle" },
  ];
  
  // Create and add buttons to the container
  controls.forEach(control => {
    const button = document.createElement('button');
    button.textContent = control.name;
    button.setAttribute('data-type', control.type);
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    
    // Format the request info based on the control
    let apiFormat = `GET: https://api.vdo.ninja/${WID}/${control.action}/null/`;
    if (control.value !== null) {
      apiFormat += typeof control.value === 'string' ? control.value : control.value;
    }
    
    tooltip.textContent = apiFormat;
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    // Set onclick handler
    button.onclick = function() {
      const url = `https://api.vdo.ninja/${WID}/${control.action}/null/${control.value !== null ? control.value : 'null'}`;
      
      addLogEntry('request', 'GET', url);
      
      fetch(url)
        .then(response => response.text())
        .then(data => {
          try {
            const jsonData = JSON.parse(data);
            addLogEntry('response', 'GET', url, null, jsonData);
          } catch (e) {
            addLogEntry('response', 'GET', url, null, data);
          }
        })
        .catch(error => {
          addLogEntry('error', 'GET', url, error.message);
        });
    };
    
    container.appendChild(button);
  });
}

// Create HTTP Group Controls
function createHTTPGroupControls() {
  const container = document.getElementById('http-groups');
  container.innerHTML = '';
  
  // Define group controls
  const groups = [1, 2];
  const actions = [
    { name: "Toggle", action: "group" },
    { name: "Join", action: "joinGroup" },
    { name: "Leave", action: "leaveGroup" }
  ];
  
  // Create and add group buttons
  groups.forEach(group => {
    actions.forEach(actionObj => {
      const button = document.createElement('button');
      button.textContent = `Group ${group}: ${actionObj.name}`;
      button.setAttribute('data-type', "toggle");
      
      // Create tooltip with request details
      const tooltip = document.createElement('span');
      tooltip.className = 'tooltip-text';
      tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
      
      // Add tooltip to button
      button.classList.add('tooltip');
      button.appendChild(tooltip);
      
      // Set onclick handler
      button.onclick = function() {
        const url = `https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
        
        addLogEntry('request', 'GET', url);
        
        fetch(url)
          .then(response => response.text())
          .then(data => {
            try {
              const jsonData = JSON.parse(data);
              addLogEntry('response', 'GET', url, null, jsonData);
            } catch (e) {
              addLogEntry('response', 'GET', url, null, data);
            }
          })
          .catch(error => {
            addLogEntry('error', 'GET', url, error.message);
          });
      };
      
      container.appendChild(button);
    });
  });
  
  // Create view group buttons
  groups.forEach(group => {
    const viewActions = [
      { name: "View Toggle", action: "viewGroup" },
      { name: "View Join", action: "joinViewGroup" },
      { name: "View Leave", action: "leaveViewGroup" }
    ];
    
    viewActions.forEach(actionObj => {
      const button = document.createElement('button');
      button.textContent = `${actionObj.name} ${group}`;
      button.setAttribute('data-type', "toggle");
      
      // Create tooltip with request details
      const tooltip = document.createElement('span');
      tooltip.className = 'tooltip-text';
      tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
      
      // Add tooltip to button
      button.classList.add('tooltip');
      button.appendChild(tooltip);
      
      // Set onclick handler
      button.onclick = function() {
        const url = `https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
        
        addLogEntry('request', 'GET', url);
        
        fetch(url)
          .then(response => response.text())
          .then(data => {
            try {
              const jsonData = JSON.parse(data);
              addLogEntry('response', 'GET', url, null, jsonData);
            } catch (e) {
              addLogEntry('response', 'GET', url, null, data);
            }
          })
          .catch(error => {
            addLogEntry('error', 'GET', url, error.message);
          });
      };
      
      container.appendChild(button);
    });
  });
  
  // Add PowerPoint control buttons
  const ppControls = [
    { name: "Next Slide", action: "nextSlide" },
    { name: "Prev Slide", action: "prevSlide" }
  ];
  
  ppControls.forEach(control => {
    const button = document.createElement('button');
    button.textContent = control.name;
    button.setAttribute('data-type', "toggle");
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${control.action}/null/null`;
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    // Set onclick handler
    button.onclick = function() {
      const url = `https://api.vdo.ninja/${WID}/${control.action}/null/null`;
      
      addLogEntry('request', 'GET', url);
      
      fetch(url)
        .then(response => response.text())
        .then(data => {
          try {
            const jsonData = JSON.parse(data);
            addLogEntry('response', 'GET', url, null, jsonData);
          } catch (e) {
            addLogEntry('response', 'GET', url, null, data);
          }
        })
        .catch(error => {
          addLogEntry('error', 'GET', url, error.message);
        });
    };
    
    container.appendChild(button);
  });
}

// Create guest control buttons
function createGuestControls() {
  const container = document.getElementById('guest-controls');
  container.innerHTML = '';
  
  // Define guest controls
  const controls = [
    { name: "Mute Mic", action: "mic", type: "toggle" },
    { name: "Toggle Camera", action: "camera", type: "toggle" },
    { name: "Toggle Video", action: "video", type: "toggle" },
    { name: "Hangup", action: "hangup", type: "false" },
    { name: "Solo Chat", action: "soloChat", type: "toggle" },
    { name: "Two-way Solo Chat", action: "soloChatBidirectional", type: "toggle" },
    { name: "Toggle Speaker", action: "speaker", type: "toggle" },
    { name: "Toggle Display", action: "display", type: "toggle" },
    { name: "Fix Rainbow", action: "forceKeyframe", type: "toggle" },
    { name: "Highlight", action: "soloVideo", type: "toggle" },
    { name: "Add to Scene 1", action: "addScene", value: 1, type: "toggle" },
    { name: "Add to Scene 2", action: "addScene", value: 2, type: "toggle" },
    { name: "Add to Group 1", action: "group", value: 1, type: "toggle" },
    { name: "Add to Group 2", action: "group", value: 2, type: "toggle" },
    { name: "Say Hello", action: "sendChat", value: "hello", type: "toggle" },
    { name: "Overlay Hello", action: "sendDirectorChat", value: "Hello", type: "toggle" },
    { name: "Start Timer (10m)", action: "startRoomTimer", value: 600, type: "toggle" },
    { name: "Pause Timer", action: "pauseRoomTimer", type: "toggle" },
    { name: "Stop Timer", action: "stopRoomTimer", type: "toggle" },
    { name: "Forward to room321", action: "forward", value: "room321", type: "toggle" },
  ];
  
  // Create and add buttons to the container
  controls.forEach(control => {
    const button = document.createElement('button');
    button.textContent = control.name;
    button.setAttribute('data-type', control.type);
    
    // Create tooltip with request details
    const tooltip = document.createElement('span');
    tooltip.className = 'tooltip-text';
    
    // Set onclick handler
    button.onclick = function() {
      const guestSlot = document.getElementById('guest-slot').value;
      const method = document.getElementById('guest-method').value;
      
      let tooltipText;
      if (method === 'websocket') {
        tooltipText = `WS: {"target":"${guestSlot}","action":"${control.action}"${control.value !== undefined ? `,"value":${typeof control.value === 'string' ? `"${control.value}"` : control.value}` : ''}}`;
      } else {
        tooltipText = `GET: https://api.vdo.ninja/${WID}/${control.action}/${guestSlot}/${control.value !== undefined ? control.value : 'null'}`;
      }
      tooltip.textContent = tooltipText;
      
      sendGuestCommand(
        guestSlot, 
        control.action, 
        control.value !== undefined ? control.value : null
      );
    };
    
    // Add tooltip to button
    button.classList.add('tooltip');
    button.appendChild(tooltip);
    
    container.appendChild(button);
  });
}

// Initialize all controls
document.addEventListener('DOMContentLoaded', function() {
  createWebSocketControls();
  createLayoutControls();
  createHTTPControls();
  createHTTPGroupControls();
  createGuestControls();
  updateHTTPUrlPreview();
  
  // Initialize form handlers for POST
  document.getElementById('post-action').addEventListener('change', function() {
    let template = { action: this.value };
    document.getElementById('post-json').value = JSON.stringify(template, null, 2);
  });
});

// Prevent accidental page reloads
window.onbeforeunload = function() {
  return "Are you sure you want to leave this page? All connection settings will be lost.";
}
</script>
</body>
</html>